package com.heima.search.service.impl;

import com.alibaba.fastjson.JSON;
import com.heima.common.exception.CustomException;
import com.heima.model.common.enums.AppHttpCodeEnum;
import com.heima.search.context.BaseContext;
import com.heima.search.dtos.SearchArticleDto;
import com.heima.search.dtos.UserSearchDto;
import com.heima.search.pojos.ApUserSearch;
import com.heima.search.service.ArticleSearchService;
import com.heima.search.vos.SearchArticleVo;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;


@Service
@Slf4j
public class ArticleSearchImpl implements ArticleSearchService {

    @Autowired
    private RestHighLevelClient restHighLevelClient;

    @Autowired
    private MongoTemplate mongoTemplate;

    /**
     * ES对搜索的文章分页查询
     *
     * @param userSearchDto 查询条件dto (关键字搜索条件 时间查询条件 分页查询条件)
     * @return 查询结果
     * @throws IOException 抛出的异常
     */
    @Override
    public List<SearchArticleDto> search(UserSearchDto userSearchDto) throws IOException {
        //参数校验
        if (userSearchDto == null || userSearchDto.getSearchWords() == null) {
            throw new CustomException(AppHttpCodeEnum.PARAM_REQUIRE);
        }

        //搜索文章时-保存用户搜索记录
        saveSearchWords(userSearchDto);


        // 1.准备Request
        SearchRequest request = new SearchRequest("app_info_article");
        // 2.准备DSL 查询条件
        //关键字+标题的搜索条件 算分
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        boolQueryBuilder.must(QueryBuilders.multiMatchQuery(userSearchDto.getSearchWords(), "title", "content"));

        // 时间查询条件 不算分
        if (userSearchDto.getMinBehotTime() != null) {
            boolQueryBuilder.filter(QueryBuilders.rangeQuery("publishTime").lte(userSearchDto.getMinBehotTime()));
        }


        // 分页查询条件 倒序
        int pageSize = userSearchDto.getPageSize();
        pageSize = pageSize < 1 ? 10 : pageSize;
        request.source().from(0).size(pageSize);
        request.source().sort("publishTime", SortOrder.DESC);

        //设置高亮
        HighlightBuilder highlightBuilder = new HighlightBuilder();
        highlightBuilder
                .field("title")
                .preTags("<font style='color: red; font-size: inherit;'>")
                .postTags("</font>");
        request.source().highlighter(highlightBuilder);

        //设置查询条件
        request.source().query(boolQueryBuilder);
        // 3.发送请求
        SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);

        // 4.解析响应
        return handleResponse(response);
    }

    /**
     * 保存用户搜索记录至MongoDB中
     * <br>
     * <b>避免ES文章分页搜索业务 和 保存用户搜索记录 两个业务同时执行影响接口响应速度,将记录搜索关键字业务放在单独线程中执行</b>
     *
     * @param userSearchDto 搜索的关键字
     */
    @Async//异步调用
    public void saveSearchWords(UserSearchDto userSearchDto) {
        //只在进入首页,查第一页内容时,才记录搜索的关键字
        if (BaseContext.getCurrentId() != null && userSearchDto.getPageNum() <= 1) {
            //查询条件
            Query query = Query.query(
                    Criteria.where("keyword").is(userSearchDto.getSearchWords())
                            .and("userId").is(BaseContext.getCurrentId())
            );
            ApUserSearch apUserSearch = mongoTemplate.findOne(query, ApUserSearch.class);
            //查询用户搜索记录是否存在
            if (apUserSearch != null) {
                //存在则更新原来记录的搜索时间
                apUserSearch.setId(apUserSearch.getId());
                apUserSearch.setCreatedTime(new Date());
                mongoTemplate.save(apUserSearch);
            } else {
                //查询当前用户搜索记录数据量是否超过10
                query = Query.query(Criteria.where("userId").is(BaseContext.getCurrentId().intValue()));
                List<ApUserSearch> apUserSearches = mongoTemplate.find(query, ApUserSearch.class);
                if (apUserSearches.size() != 0 && apUserSearches.size() >= 10) {
                    //超过10,将最后一条最久的搜索记录替换为当前搜索记录
                    //搜索时间降序排序,获取最后一条记录,并替换
                    query.with(Sort.by(Sort.Direction.DESC, "createdTime"));//降序
                    apUserSearches = mongoTemplate.find(query, ApUserSearch.class);
                    apUserSearch = apUserSearches.get(apUserSearches.size() - 1);//最后一条记录
                    apUserSearch.setKeyword(userSearchDto.getSearchWords());
                    apUserSearch.setCreatedTime(new Date());//最新搜索时间
                    mongoTemplate.save(apUserSearch);
                } else {
                    //没有超过10,直接保存当前记录
                    apUserSearch = new ApUserSearch();
                    apUserSearch.setUserId(BaseContext.getCurrentId().intValue());
                    apUserSearch.setKeyword(userSearchDto.getSearchWords());
                    apUserSearch.setCreatedTime(new Date());//最新搜索时间
                    mongoTemplate.save(apUserSearch);
                }

            }
        }
    }

    /**
     * 将文章微服务发布的文章 同步至es中
     *
     * @param searchArticleVo 同步的消息
     */
    @Override
    public void esSync(String searchArticleVoStr) {
        log.info("searchArticleVoStr=" + searchArticleVoStr);
        try {
            SearchArticleVo searchArticleVo = JSON.parseObject(searchArticleVoStr, SearchArticleVo.class);
            // 1.准备Request对象
            IndexRequest request = new IndexRequest("app_info_article").id(searchArticleVo.getId().toString());
            // 2.准备Json文档
            request.source(JSON.toJSONString(searchArticleVo), XContentType.JSON);
            // 3.发送请求
            restHighLevelClient.index(request, RequestOptions.DEFAULT);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 解析响应
     *
     * @param response es客户端发送的请求
     * @return 返回的结果
     */
    private List<SearchArticleDto> handleResponse(SearchResponse response) {
        // 存放搜索的结果
        List<SearchArticleDto> searchArticleDtoList = new ArrayList<>();

        // 4.解析响应
        SearchHits searchHits = response.getHits();
        // 4.1.获取总条数
        long total = searchHits.getTotalHits().value;
        log.info("共搜索到" + total + "条数据");
        // 4.2.文档数组
        SearchHit[] hits = searchHits.getHits();
        // 4.3.遍历
        for (SearchHit hit : hits) {
            // 获取文档source
            String json = hit.getSourceAsString();
            // 反序列化
            SearchArticleDto searchArticleDto = JSON.parseObject(json, SearchArticleDto.class);
            //5 获取高亮结果
            Map<String, HighlightField> highlightFields = hit.getHighlightFields();
            if (!CollectionUtils.isEmpty(highlightFields)) {
                // 根据字段名获取高亮结果
                HighlightField highlightField = highlightFields.get("title");
                if (highlightField != null) {
                    log.info("搜索到title中包含的内容" + searchArticleDto.getTitle());
                    // 获取高亮值
                    StringBuilder stringBuilder = new StringBuilder();
                    for (Text fragment : highlightField.getFragments()) {
                        stringBuilder.append(fragment.string());
                    }
                    // 覆盖非高亮结果
                    searchArticleDto.setHTitle(stringBuilder.toString());
                    searchArticleDtoList.add(searchArticleDto);
                }
            }
        }

        return searchArticleDtoList;
    }
}
